/*
 * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2006.
 *
 * Licensed under the Aduna BSD-style license.
 */
 
package edu.tju.commons.io;

import java.io.IOException;
import java.io.Reader;

/**
 * A character-stream reader that allows look-aheads to be performed. The reader
 * does not look ahead any more than necesarry, so performing a look ahead of 5
 * characters means that max. 5 characters will be read into the look ahead
 * buffer. The look ahead buffer grows as needed.
 * <p>
 * <b>Note that this implementation is not synchronized.</b>
 * <p>
 * <b>FIXME:</b> implementation does too many array copies. Using a 'sliding
 * window' in the buffer is cheaper.
 */
public class LookAheadReader extends Reader {

	/** The reader containing the source data. * */
	private Reader in = null;

	/** The look ahead buffer. * */
	private char[] laBuf;

	/** The current number of characters in the LA buffer. * */
	private int curLA = 0;

	/**
	 * Creates a new LookAheadReader.
	 *
	 * @param r
	 *        The reader to do the look ahead on
	 * @param bufSize
	 *        The initial size of the look ahead buffer. Minimum size is 1
	 *        (needed for 'moreInput()').
	 */
	public LookAheadReader(Reader r, int bufSize) {
		in = r;

		if (bufSize >= 1) {
			laBuf = new char[bufSize];
		}
		else {
			laBuf = new char[1];
		}
	}

	/**
	 * Creates a new LookAheadReader with a initial buffer size of 8 characters.
	 *
	 * @param r
	 *        The reader to do the look ahead on
	 */
	public LookAheadReader(Reader r) {
		this(r, 8);
	}

	/**
	 * Check whether the next input is equal to the supplied string
	 * (case-sensitive).
	 *
	 * @param s
	 *        The string to compare to the input
	 * @return true if the input is equal to the supplied string, false
	 *         otherwise
	 * @exception IOException
	 *            If an I/O error occurred in the current Reader or if the
	 *            string is longer than the maximum look ahead.
	 */
	public boolean nextInputIs(String s) throws IOException {
		// Enough characters in laBuf?
		int inBuf = fillLABuf(s.length());

		if (s.length() > inBuf) {
			// Not that much characters in stream anymore
			return false;
		}

		// Compare string to characters in LA buffer
		for (int i = 0; i < s.length(); i++) {
			if (laBuf[i] != s.charAt(i)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Check whether the next input is equal to the supplied string
	 * (case-insensitive).
	 *
	 * @param s
	 *        The string to compare to the input
	 * @return true if the input is equal to the supplied string, false
	 *         otherwise
	 * @exception IOException
	 *            If an I/O error occurred in the current Reader or if the
	 *            string is longer than the maximum look ahead.
	 */
	public boolean nextInputIgnoreCaseIs(String s) throws IOException {
		// Enough characters in laBuf?
		int inBuf = fillLABuf(s.length());

		if (s.length() > inBuf) {
			// Not that much characters in stream anymore
			return false;
		}

		// Compare string to characters in LA buffer
		for (int i = 0; i < s.length(); i++) {
			if (Character.toUpperCase(laBuf[i]) != Character.toUpperCase(s.charAt(i))) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Get the character at the supplied lookAhead. charAt(0) returns the first
	 * character to be read.
	 *
	 * @return the character at the supplied lookAhead, or -1 if the lookAhead
	 *         is after the last character in the stream.
	 */
	public int charAt(int lookAhead) throws IOException {
		// Fill buffer in necessary
		int inBuf = fillLABuf(lookAhead + 1);

		// Enough characters in buffer?
		if (lookAhead >= inBuf) {
			// Not that much characters in stream anymore
			return -1;
		}

		return laBuf[lookAhead];
	}

	/**
	 * Checks whether there's more input to read.
	 *
	 * @exception IOException
	 *            If an I/O error occurred in the current Reader
	 */
	public boolean moreInput() throws IOException {
		int inBuf = fillLABuf(1);
		return (inBuf > 0);
	}

	/**
	 * Tries to fill the look ahead buffer so that it contains 'charsNeeded'
	 * characters. This will succeed, except when the end of the stream has been
	 * reached.
	 *
	 * @return The number of characters in the buffer
	 */
	public int fillLABuf(int charsNeeded) throws IOException {
		// Calculate how much characters need to be read
		int toRead = charsNeeded - curLA;

		if (toRead > 0) {
			// Check if look ahead buffer is large enough
			if (charsNeeded > laBuf.length) {
				char[] newBuf = new char[charsNeeded];
				System.arraycopy(laBuf, 0, newBuf, 0, curLA);
				laBuf = newBuf;
			}

			// Fill the buffer
			int charsRead = in.read(laBuf, curLA, toRead);
			if (charsRead != -1) {
				curLA += charsRead;
			}
		}
		return curLA;
	}

	@Override
	public int read() throws IOException {
		int result;

		if (curLA == 0) {
			result = in.read();
		}
		else {
			// return result from LA buffer
			result = laBuf[0];

			// Shift all characters one position
			curLA -= 1;
			System.arraycopy(laBuf, 1, laBuf, 0, curLA);
		}
		return result;
	}

	@Override
	public int read(char[] cbuf, int off, int len) throws IOException {
		if (curLA == 0) {
			// Get all chars from stream
			return in.read(cbuf, off, len);
		}
		else if (len <= curLA) {
			// Get all chars from LA buffer
			System.arraycopy(laBuf, 0, cbuf, off, len);

			// Shift all characters 'len' positions
			curLA -= len;
			if (curLA > 0) {
				System.arraycopy(laBuf, len, laBuf, 0, curLA);
			}

			return len;
		}
		else {
			// Combine chars from buffer with chars from stream
			System.arraycopy(laBuf, 0, cbuf, off, curLA);
			int charsRead = curLA;
			curLA = 0;

			int fromStream = in.read(cbuf, off + curLA, len - curLA);
			if (fromStream != -1) {
				charsRead += fromStream;
			}

			return charsRead;
		}
	}

	@Override
	public long skip(long n) throws IOException {
		if (curLA == 0) {
			// Only skip chars from stream
			return in.skip(n);
		}
		else if (n <= curLA) {
			// Only skip chars from LA buffer
			curLA -= n;
			if (curLA > 0) {
				System.arraycopy(laBuf, (int) n, laBuf, 0, curLA);
			}

			return n;
		}
		else {
			// Skip all chars from buffer and some from stream
			long skipped = curLA;
			curLA = 0;
			skipped += in.skip(n);

			return skipped;
		}
	}

	@Override
	public void close()
		throws IOException {
		curLA = 0;
		in.close();
	}
}
